<!DOCTYPE html>
<!--
Copyright (c) 2014 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->
<link rel="import" href="/tracing/base/utils.html">
<script>
'use strict';
/* eslint-disable no-console */

tr.exportTo('tr.b', function() {
  const ESTIMATED_IDLE_PERIOD_LENGTH_MILLISECONDS = 10;
  // The maximum amount of time that we allow for a task to get scheduled
  // in idle time before forcing the task to run.
  const REQUEST_IDLE_CALLBACK_TIMEOUT_MILLISECONDS = 100;

  // Setting this to true will cause stack traces to get dumped into the
  // tasks. When an exception happens the original stack will be printed.
  //
  // NOTE: This should never be set committed as true.
  const recordRAFStacks = false;

  let pendingPreAFs = [];
  let pendingRAFs = [];
  const pendingIdleCallbacks = [];
  let currentRAFDispatchList = undefined;

  let rafScheduled = false;
  let idleWorkScheduled = false;

  function scheduleRAF() {
    if (rafScheduled) return;
    rafScheduled = true;
    if (tr.isHeadless) {
      Promise.resolve().then(function() {
        processRequests(false, 0);
      }, function(e) {
        throw e;
      });
    } else {
      if (window.requestAnimationFrame) {
        window.requestAnimationFrame(processRequests.bind(this, false));
      } else {
        const delta = Date.now() - window.performance.now();
        window.webkitRequestAnimationFrame(function(domTimeStamp) {
          processRequests(false, domTimeStamp - delta);
        });
      }
    }
  }

  function nativeRequestIdleCallbackSupported() {
    return !tr.isHeadless && window.requestIdleCallback;
  }

  function scheduleIdleWork() {
    if (idleWorkScheduled) return;
    if (!nativeRequestIdleCallbackSupported()) {
      scheduleRAF();
      return;
    }
    idleWorkScheduled = true;
    window.requestIdleCallback(function(deadline, didTimeout) {
      processIdleWork(false /* forceAllTasksToRun */, deadline);
    }, { timeout: REQUEST_IDLE_CALLBACK_TIMEOUT_MILLISECONDS });
  }

  function onAnimationFrameError(e, opt_stack) {
    console.log(e.stack);
    if (tr.isHeadless) throw e;

    if (opt_stack) console.log(opt_stack);

    if (e.message) {
      console.error(e.message, e.stack);
    } else {
      console.error(e);
    }
  }

  function runTask(task, frameBeginTime) {
    try {
      task.callback.call(task.context, frameBeginTime);
    } catch (e) {
      tr.b.onAnimationFrameError(e, task.stack);
    }
  }

  function processRequests(forceAllTasksToRun, frameBeginTime) {
    rafScheduled = false;

    const currentPreAFs = pendingPreAFs;
    currentRAFDispatchList = pendingRAFs;
    pendingPreAFs = [];
    pendingRAFs = [];
    const hasRAFTasks = currentPreAFs.length || currentRAFDispatchList.length;

    for (let i = 0; i < currentPreAFs.length; i++) {
      runTask(currentPreAFs[i], frameBeginTime);
    }

    while (currentRAFDispatchList.length > 0) {
      runTask(currentRAFDispatchList.shift(), frameBeginTime);
    }
    currentRAFDispatchList = undefined;

    if ((!hasRAFTasks && !nativeRequestIdleCallbackSupported()) ||
        forceAllTasksToRun) {
      // We assume that we want to do a fixed maximum amount of optional work
      // per frame. Hopefully rAF will eventually pass this in for us.
      const rafCompletionDeadline =
          frameBeginTime + ESTIMATED_IDLE_PERIOD_LENGTH_MILLISECONDS;
      processIdleWork(
          forceAllTasksToRun, {
            timeRemaining() {
              return rafCompletionDeadline - window.performance.now();
            }
          }
      );
    }

    if (pendingIdleCallbacks.length > 0) scheduleIdleWork();
  }

  function processIdleWork(forceAllTasksToRun, deadline) {
    idleWorkScheduled = false;
    while (pendingIdleCallbacks.length > 0) {
      runTask(pendingIdleCallbacks.shift());
      // Check timer after running at least one idle task to avoid buggy
      // window.performance.now() on some platforms from blocking the idle
      // task queue.
      if (!forceAllTasksToRun &&
          (tr.isHeadless || deadline.timeRemaining() <= 0)) {
        break;
      }
    }

    if (pendingIdleCallbacks.length > 0) scheduleIdleWork();
  }

  function getStack_() {
    if (!recordRAFStacks) return '';

    const stackLines = tr.b.stackTrace();
    // Strip off getStack_.
    stackLines.shift();
    return stackLines.join('\n');
  }

  function requestPreAnimationFrame(callback, opt_this) {
    pendingPreAFs.push({
      callback,
      context: opt_this || global,
      stack: getStack_()});
    scheduleRAF();
  }

  function requestAnimationFrameInThisFrameIfPossible(callback, opt_this) {
    if (!currentRAFDispatchList) {
      requestAnimationFrame(callback, opt_this);
      return;
    }
    currentRAFDispatchList.push({
      callback,
      context: opt_this || global,
      stack: getStack_()});
    return;
  }

  function requestAnimationFrame(callback, opt_this) {
    pendingRAFs.push({
      callback,
      context: opt_this || global,
      stack: getStack_()});
    scheduleRAF();
  }

  function animationFrame() {
    return new Promise(resolve => requestAnimationFrame(resolve));
  }

  function requestIdleCallback(callback, opt_this) {
    pendingIdleCallbacks.push({
      callback,
      context: opt_this || global,
      stack: getStack_()});
    scheduleIdleWork();
  }

  function forcePendingRAFTasksToRun(frameBeginTime) {
    if (!rafScheduled) return;
    processRequests(false, frameBeginTime);
  }

  function forceAllPendingTasksToRunForTest() {
    if (!rafScheduled && !idleWorkScheduled) return;
    processRequests(true, 0);
  }

  function timeout(ms) {
    return new Promise(resolve => window.setTimeout(resolve, ms));
  }

  function idle() {
    return new Promise(resolve => requestIdleCallback(resolve));
  }

  return {
    animationFrame,
    forceAllPendingTasksToRunForTest,
    forcePendingRAFTasksToRun,
    idle,
    onAnimationFrameError,
    requestAnimationFrame,
    requestAnimationFrameInThisFrameIfPossible,
    requestIdleCallback,
    requestPreAnimationFrame,
    timeout,
  };
});
</script>
